package business

import (
	"sort"
	"sync"

	"gopkg.in/yaml.v2"
	"k8s.io/apimachinery/pkg/api/errors"

	"github.com/kiali/kiali/config"
	"github.com/kiali/kiali/kubernetes"
	"github.com/kiali/kiali/models"
)

type IstioCertsService struct {
	k8s           kubernetes.ClientInterface
	businessLayer *Layer
}

type certConfig struct {
	SecretName string   `yaml:"secretName"`
	DNSNames   []string `yaml:"dnsNames"`
}

type istioConfig struct {
	Certificates []certConfig `yaml:"certificates"`
}

const (
	IstioDefaultCASecret string = "istio-ca-secret"
	UserProvidedCASecret string = "cacerts"
	CACert               string = "ca-cert.pem"
	CAChainCert          string = "cert-chain.pem"
)

func (ics *IstioCertsService) GetCertsInfo() ([]models.CertInfo, error) {
	// Return an empty list if the feature is not enabled
	if !config.Get().KialiFeatureFlags.CertificatesInformationIndicators.Enabled {
		return []models.CertInfo{}, nil
	}

	// Check if there are certificates configured in Istio ConfigMap
	certsConfig, err := ics.getCertsConfigFromIstioConfigMap()
	if err != nil {
		return nil, err
	}
	if certsConfig != nil {
		// If there are, get the certificates generated by Chiron
		certs, err := ics.getChironCertificates(certsConfig)
		if err != nil {
			return nil, err
		}
		return certs, nil
	}

	// Check if there is a user provided secret
	cert, err := ics.getCertificateFromSecret(UserProvidedCASecret, CACert)
	if err == nil {
		return []models.CertInfo{cert}, nil
	}
	if !errors.IsNotFound(err) {
		// Return an error unless the secret is not found (this secret is not mandatory)
		return nil, err
	}

	// Get the default certificate generated by Istio
	cert, err = ics.getCertificateFromSecret(IstioDefaultCASecret, CACert)
	if err != nil {
		return nil, err
	}
	return []models.CertInfo{cert}, nil
}

func (ics *IstioCertsService) getCertificateFromSecret(secretName, certName string) (models.CertInfo, error) {
	cfg := config.Get()

	cert := models.CertInfo{}
	cert.SecretName = secretName
	cert.SecretNamespace = cfg.IstioNamespace

	secret, err := ics.k8s.GetSecret(cfg.IstioNamespace, secretName)

	if err != nil {
		if errors.IsForbidden(err) {
			cert.Accessible = false
			return cert, nil
		}
		return models.CertInfo{}, err
	}

	cert.Parse(secret.Data[certName])

	return cert, nil
}

func (ics *IstioCertsService) getCertsConfigFromIstioConfigMap() ([]certConfig, error) {
	cfg := config.Get()

	istioConfigMap, err := ics.k8s.GetConfigMap(cfg.IstioNamespace, cfg.ExternalServices.Istio.ConfigMapName)
	if err != nil {
		return nil, err
	}

	istioConfig := istioConfig{}
	err = yaml.Unmarshal([]byte(istioConfigMap.Data["mesh"]), &istioConfig)
	if err != nil {
		return nil, err
	}

	return istioConfig.Certificates, nil
}

func (ics *IstioCertsService) getChironCertificates(certsConfig []certConfig) ([]models.CertInfo, error) {
	wg := sync.WaitGroup{}
	certChan := make(chan models.CertInfo, len(certsConfig))
	errChan := make(chan error, len(certsConfig))

	for _, certConfig := range certsConfig {
		wg.Add(1)
		go func(secretName string, dnsNames []string) {
			defer wg.Done()

			cert, err := ics.getCertificateFromSecret(secretName, CAChainCert)
			if err != nil {
				errChan <- err
				return
			}

			cert.DNSNames = dnsNames
			certChan <- cert

		}(certConfig.SecretName, certConfig.DNSNames)
	}

	wg.Wait()
	close(certChan)
	close(errChan)

	for err := range errChan {
		if err != nil {
			return nil, err
		}
	}

	certs := make([]models.CertInfo, 0)
	for cert := range certChan {
		certs = append(certs, cert)
	}

	sort.Slice(certs, func(i, j int) bool {
		return certs[i].SecretName < certs[j].SecretName
	})

	return certs, nil
}
